/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A 
 * copy of the license can be found in the License.html file at the root of this distribution. If 
 * you cannot locate the  Microsoft Public License, please send an email to 
 * dlr@microsoft.com. By using this source code in any fashion, you are agreeing to be bound 
 * by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Scripting.Runtime;
using Microsoft.Scripting.Utils;

namespace Microsoft.Scripting.Ast {
    /// <summary>
    /// The builder for creating the LambdaExpression node.
    /// 
    /// Since the nodes require that parameters and variables are created
    /// before hand and then passed to the factories creating LambdaExpression
    /// this builder keeps track of the different pieces and at the end creates
    /// the LambdaExpression.
    /// 
    /// TODO: This has some functionality related to CodeContext that should be
    /// removed, in favor of languages handling their own local scopes
    /// </summary>
    public class LambdaBuilder {
        private readonly List<VariableExpression> _locals = new List<VariableExpression>();
        private readonly List<ParameterExpression> _params = new List<ParameterExpression>();
        private readonly List<Expression> _visibleLocals = new List<Expression>();
        private readonly List<Expression> _visibleParams = new List<Expression>();
        private Annotations _annotations;
        private string _name;
        private Type _returnType;
        private MethodInfo _contextFactory;
        private ParameterExpression _paramsArray;
        private Expression _body;
        private bool _dictionary;
        private bool _global;
        private bool _visible = true;
        private bool _completed;

        internal LambdaBuilder(Annotations annotations, string name, Type returnType) {
            _annotations = annotations;
            _name = name;
            _returnType = returnType;
        }

        /// <summary>
        /// Annotations for the lambda being built
        /// </summary>
        public Annotations Annotations {
            get {
                return _annotations;
            }
            set {
                _annotations = value;
            }
        }

        /// <summary>
        /// The name of the lambda.
        /// Currently anonymous/unnamed lambdas are not allowed.
        /// </summary>
        public string Name {
            get {
                return _name;
            }
            set {
                ContractUtils.RequiresNotNull(value, "value");
                _name = value;
            }
        }

        /// <summary>
        /// Return type of the lambda being created.
        /// </summary>
        public Type ReturnType {
            get {
                return _returnType;
            }
            set {
                ContractUtils.RequiresNotNull(value, "value");
                _returnType = value;
            }
        }

        /// <summary>
        /// Optional factory method used for CodeContext instantiation.
        /// Factory must have signature compatible with: CodeContext Factory(ILocalVariables, CodeContext, bool)
        /// TODO: remove !!!
        /// </summary>
        public MethodInfo ScopeFactory {
            get {
                return _contextFactory;
            }
            set {
                if (value != null) {
                    ValidateContextFactory(value, "value");
                }
                _contextFactory = value;
            }
        }

        /// <summary>
        /// List of lambda's local variables for direct manipulation.
        /// </summary>
        public List<VariableExpression> Locals {
            get {
                return _locals;
            }
        }

        /// <summary>
        /// List of lambda's parameters for direct manipulation
        /// </summary>
        public List<ParameterExpression> Parameters {
            get {
                return _params;
            }
        }

        /// <summary>
        /// The params array argument, if any.
        /// </summary>
        public ParameterExpression ParamsArray {
            get {
                return _paramsArray;
            }
        }

        /// <summary>
        /// The body of the lambda. This must be non-null.
        /// </summary>
        public Expression Body {
            get {
                return _body;
            }
            set {
                ContractUtils.RequiresNotNull(value, "value");
                _body = value;
            }
        }

        /// <summary>
        /// The generated lambda should have dictionary of locals
        /// instead of allocating them directly on the CLR stack.
        /// </summary>
        public bool Dictionary {
            get {
                return _dictionary;
            }
            set {
                _dictionary = value;
            }
        }

        /// <summary>
        /// The resulting lambda should be marked as global.
        /// TODO: remove !!!
        /// </summary>
        public bool Global {
            get {
                return _global;
            }
            set {
                _global = value;
            }
        }

        /// <summary>
        /// The scope is visible (default). Invisible if false.
        /// </summary>
        public bool Visible {
            get {
                return _visible;
            }
            set {
                _visible = value;
            }
        }

        /// <summary>
        /// Creates a parameter on the lambda with a given name and type.
        /// 
        /// Parameters maintain the order in which they are created,
        /// however custom ordering is possible via direct access to
        /// Parameters collection.
        /// </summary>
        public ParameterExpression CreateParameter(string name, Type type) {
            ContractUtils.RequiresNotNull(type, "type");            
            ParameterExpression result = Expression.Parameter(type, name);
            _params.Add(result);
            _visibleParams.Add(result);
            return result;
        }


        /// <summary>
        /// Creates a hidden parameter on the lambda with a given name and type.
        /// 
        /// Parameters maintain the order in which they are created,
        /// however custom ordering is possible via direct access to
        /// Parameters collection.
        /// </summary>
        public ParameterExpression CreateHiddenParameter(string name, Type type) {
            ContractUtils.RequiresNotNull(type, "type");
            ParameterExpression result = Expression.Parameter(type, name);
            _params.Add(result);
            return result;
        }

        /// <summary>
        /// Creates a params array argument on the labmda.
        /// 
        /// The params array argument is added to the signature immediately. Before the lambda is
        /// created, the builder validates that it is still the last (since the caller can modify
        /// the order of parameters explicitly by maniuplating the parameter list)
        /// </summary>
        public ParameterExpression CreateParamsArray(string name, Type type) {
            ContractUtils.RequiresNotNull(type, "type");
            ContractUtils.Requires(type.IsArray, "type");
            ContractUtils.Requires(type.GetArrayRank() == 1, "type");
            ContractUtils.Requires(_paramsArray == null, "type", "Already have parameter array");

            return _paramsArray = CreateParameter(name, type);
        }

        /// <summary>
        /// Creates a global variable with specified name and type.
        /// TODO: remove
        /// </summary>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
        public Expression CreateGlobalVariable(string name, Type type) {
            return Expression.Global(type, name);
        }

        /// <summary>
        /// Creates a local variable with specified name and type.
        /// </summary>
        public Expression CreateLocalVariable(string name, Type type) {
            if (_global) {
                // special treatment of lambdas marked as global
                return Expression.Global(type, name, true);
            }

            VariableExpression result = Expression.Variable(type, name);
            _locals.Add(result);
            _visibleLocals.Add(result);
            return result;
        }

        /// <summary>
        /// Creates a temporary variable with specified name and type.
        /// </summary>
        public VariableExpression CreateTemporaryVariable(string name, Type type) {
            VariableExpression result = Expression.Variable(type, name);
            _locals.Add(result);
            return result;
        }

        /// <summary>
        /// Creates a temporary variable with specified name and type.
        /// </summary>
        public VariableExpression Temporary(Type type, string name) {
            return CreateTemporaryVariable(name, type);
        }

        /// <summary>
        /// Adds the temporary variable to the list of variables maintained
        /// by the builder. This is useful in cases where the variable is
        /// created outside of the builder.
        /// </summary>
        public void AddTemp(VariableExpression temp) {
            ContractUtils.RequiresNotNull(temp, "temp");
            _locals.Add(temp);
        }

        /// <summary>
        /// Creates the LambdaExpression from the builder.
        /// After this operation, the builder can no longer be used to create other instances.
        /// </summary>
        /// <param name="lambdaType">Desired type of the lambda. </param>
        /// <returns>New LambdaExpression instance.</returns>
        public LambdaExpression MakeLambda(Type lambdaType) {
            Validate();

            LambdaExpression lambda = Expression.Lambda(
                lambdaType,
                MakeBody(false),
                _name,
                _returnType,
                _paramsArray != null,
                _annotations,
                ToArray(_params)
            );

            // The builder is now completed
            _completed = true;

            return lambda;
        }

        /// <summary>
        /// Creates the LambdaExpression from the builder.
        /// After this operation, the builder can no longer be used to create other instances.
        /// </summary>
        /// <returns>New LambdaExpression instance.</returns>
        public LambdaExpression MakeLambda() {
            ContractUtils.Requires(_paramsArray == null, "Paramarray lambdas require explicit delegate type");
            Validate();
            LambdaExpression lambda = Expression.Lambda(MakeBody(false), _name, _returnType, _annotations, ToArray(_params));

            // The builder is now completed
            _completed = true;

            return lambda;
        }


        /// <summary>
        /// Creates the generator LambdaExpression from the builder.
        /// After this operation, the builder can no longer be used to create other instances.
        /// </summary>
        /// <returns>New LambdaExpression instance.</returns>
        public LambdaExpression MakeGenerator(Type lambdaType) {
            Validate();
            LambdaExpression lambda = Expression.Generator(lambdaType, _name, MakeBody(true), _annotations, ToArray(_params));

            // The builder is now completed
            _completed = true;

            return lambda;
        }

        /// <summary>
        /// Validates that the builder has enough information to create the lambda.
        /// </summary>
        private void Validate() {
            if (_completed) {
                throw new InvalidOperationException("The builder is closed");
            }
            if (_returnType == null) {
                throw new InvalidOperationException("Return type is missing");
            }
            if (_name == null) {
                throw new InvalidOperationException("Name is missing");
            }
            if (_body == null) {
                throw new InvalidOperationException("Body is missing");
            }

            if (_paramsArray != null &&
                (_params.Count == 0 || _params[_params.Count -1] != _paramsArray)) {
                throw new InvalidOperationException("The params array parameter is not last in the parameter list");
            }
        }


        private Expression MakeBody(bool generator) {
            Expression body = _body;

            bool emitDictionary = _dictionary || ScriptDomainManager.Options.Frames;
            bool needsContext = !_global || _contextFactory != null || generator;

            // wrap a CodeContext scope if needed
            if (needsContext) {
                MethodInfo factory = _contextFactory ?? typeof(RuntimeOps).GetMethod("CreateNestedCodeContext");

                List<Expression> vars = new List<Expression>(_visibleParams);
                vars.AddRange(_visibleLocals);

                Expression localScope = emitDictionary ? Expression.AllVariables(vars) : Expression.LiftedVariables(vars);

                body = Expression.CodeContextScope(
                    body,
                    Expression.Call(
                        factory,
                        localScope,
                        Expression.CodeContext(),
                        Expression.Constant(_visible)
                    )
                );
            }

            // wrap a scope if needed
            if (_locals != null && _locals.Count > 0) {
                body = Expression.Scope(body, _name, _locals.ToArray());
            }
            return body;
        }

        private static T[] ToArray<T>(List<T> list) {
            return list != null ? list.ToArray() : new T[0];
        }

        internal static void ValidateContextFactory(MethodInfo factory, string paramName) {
            ContractUtils.RequiresNotNull(factory, "factory");
            ContractUtils.Requires(
                IsValidScopeFactory(factory),
                paramName,
                "Factory must have signature compatible with: CodeContext Factory(ILocalVariables, CodeContext, bool)"
            );
        }

        internal static bool IsValidScopeFactory(MethodInfo factory) {
            ParameterInfo[] ps = factory.GetParameters();
            return !factory.IsGenericMethod &&
                (typeof(CodeContext).IsAssignableFrom(factory.ReturnType)) &&
                (ps.Length == 3) &&
                (ps[0].ParameterType == typeof(ILocalVariables)) &&
                (ps[1].ParameterType == typeof(CodeContext)) &&
                (ps[2].ParameterType == typeof(bool));
        }

        #region OBSOLETE API

        public ParameterExpression CreateParameter(/*REMOVE*/SymbolId name, Type type) {
            return CreateParameter(SymbolTable.IdToString(name), type);
        }

        public ParameterExpression CreataParamsArray(/*REMOVE*/SymbolId name, Type type) {
            return CreateParamsArray(SymbolTable.IdToString(name), type);
        }

        public Expression CreateGlobalVariable(/*REMOVE*/SymbolId name, Type type) {
            return CreateGlobalVariable(SymbolTable.IdToString(name), type);
        }

        public Expression CreateLocalVariable(/*REMOVE*/SymbolId name, Type type) {
            return CreateLocalVariable(SymbolTable.IdToString(name), type);
        }

        public VariableExpression CreateTemporaryVariable(/*REMOVE*/SymbolId name, Type type) {
            return CreateTemporaryVariable(SymbolTable.IdToString(name), type);
        }

        #endregion
    }

    public static partial class Utils {
        /// <summary>
        /// Creates new instance of the LambdaBuilder with specified name, return type and a source span.
        /// </summary>
        /// <param name="returnType">Return type of the lambda being built.</param>
        /// <param name="name">Name of the lambda being built.</param>
        /// <param name="span">SourceSpan for the lambda being built.</param>
        /// <returns>New instance of the </returns>
        public static LambdaBuilder Lambda(Type returnType, string name, SourceSpan span) {
            return Lambda(returnType, name, Expression.Annotate(span));
        }

        /// <summary>
        /// Creates new instnace of the LambdaBuilder with specified name and a return type.
        /// </summary>
        /// <param name="returnType">Return type of the lambda being built.</param>
        /// <param name="name">Name for the lambda being built.</param>
        /// <returns>new LambdaBuilder instance</returns>
        public static LambdaBuilder Lambda(Type returnType, string name) {
            return Lambda(returnType, name, Annotations.Empty);
        }

        public static LambdaBuilder Lambda(Type returnType, string name, Annotations annotations) {
            return new LambdaBuilder(annotations, name, returnType);
        }

        // TODO: remove !!!
        [Obsolete("use Expression.CodeContextScope instead")]
        public static CodeContextScopeExpression Scope(Expression/*!*/ body, MethodInfo/*!*/ factory) {
            ContractUtils.RequiresNotNull(body, "body");
            LambdaBuilder.ValidateContextFactory(factory, "factory");

            return Expression.CodeContextScope(
                Expression.Void(body),
                Expression.Call(
                    factory,
                    Expression.Null(typeof(ILocalVariables)),
                    Expression.CodeContext(),
                    Expression.Constant(true)
                )
            );
        }
    }
}

namespace Microsoft.Scripting.Runtime {
    public static partial class RuntimeOps {
        public static CodeContext CreateNestedCodeContext(ILocalVariables locals, CodeContext context, bool visible) {
            if (locals.Count == 0 && context.Scope.IsVisible == visible) {
                // performance: just reuse old context if this scope has no variables.
                return context;
            }

            return new CodeContext(new Scope(context.Scope, new LocalsDictionary(locals), visible), context.LanguageContext, context);
        }
    }
}